16 Glide 自定义模块功能

项目地址:https://github.com/bumptech/glide
官方文档:http://bumptech.github.io/glide/
中文文档:https://muyangmin.github.io/glide-docs-cn/
原文链接:https://blog.csdn.net/guolin_blog/article/details/78179422
·
文章目录:
Glide 快速入门
Glide 源码分析执行流程
Glide 缓存机制
Glide 回调与监听
Glide 图片变换功能
Glide 自定义模块功能

自定义模块的基本用法

学到这里相信你已经知道,Glide 的用法是非常非常简单的,大多数情况下,我们想要实现的图片加载效果只需要一行代码就能解决了。但是 Glide 过于简洁的 API 也造成了一个问题,就是如果我们想要更改 Glide 的某些默认配置项应该怎么操作呢?很难想象如何将更改 Glide 配置项的操作串联到一行经典的 Glide 图片加载语句中当中吧?没错,这个时候就需要用到自定义模块功能了。

自定义模块功能可以将更改 Glide 配置,替换 Glide 组件等操作独立出来,使得我们能轻松地对 Glide 的各种配置进行自定义,并且又和 Glide 的图片加载逻辑没有任何交集,这也是一种低耦合编程方式的体现。那么接下来我们就学习一下自定义模块的基本用法。

首先需要定义一个我们自己的模块类,并让它实现 GlideModule 接口,如下所示:

1
2
3
4
5
6
7
8
9
public class MyGlideModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
}
@Override
public void registerComponents(Context context, Glide glide) {
}
}

可以看到,在 MyGlideModule 类当中,我们重写了 applyOptions() 和 registerComponents() 方法,这两个方法分别就是用来更改 Glide 和配置以及替换 Glide 组件的。我们待会儿只需要在这两个方法中加入具体的逻辑,就能实现更改 Glide 配置或者替换 Glide 组件的功能了。

不过,目前 Glide 还无法识别我们自定义的 MyGlideModule,如果想要让它生效,还得在 AndroidManifest.xml 文件当中加入如下配置才行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<manifest>
...
<application>
<meta-data
android:name="com.example.glidetest.MyGlideModule"
android:value="GlideModule" />
...
</application>
</manifest>

标签中加入一个 meta-data 配置项,其中 android:name 指定成我们自定义的 MyGlideModule 的完整路径, android:value 必须指定成 GlideModule,这个是固定值。

这样的话,我们就将 Glide 自定义模块的功能完成了,是不是非常简单?现在 Glide 已经能够识别我们自定义的这个 MyGlideModule 了,但是在编写具体的功能之前,我们还是按照老规矩阅读一下源码,从源码的层面上来分析一下,Glide 到底是如何识别出这个自定义的 MyGlideModule 的。

自定义模块的原理

这里我不会带着大家从 Glide 代码执行的第一步一行行重头去解析 Glide 的源码,而是只分析和自定义模块相关的部分。

显然我们已经用惯了 Glide.with(context).load(url).into(imageView) 这样一行简洁的 Glide 图片加载语句,但是我们好像从来没有注意过 Glide 这个类本身的实例。然而事实上,Glide 类确实是有创建实例的,只不过是在内部由 Glide 自动帮我们创建和管理了,对于开发者而言,大多数情况下是不用关心它的,只需要调用它的静态方法就可以了。

那么 Glide 的实例到底是在哪里创建的呢?我们来看下 Glide 类中的 get() 方法的源码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Glide {
private static volatile Glide glide;
...
public static Glide get(Context context) {
if (glide == null) {
synchronized (Glide.class) {
if (glide == null) {
Context applicationContext = context.getApplicationContext();
List<GlideModule> modules = new ManifestParser(applicationContext).parse();
GlideBuilder builder = new GlideBuilder(applicationContext);
for (GlideModule module : modules) {
module.applyOptions(applicationContext, builder);
}
glide = builder.createGlide();
for (GlideModule module : modules) {
module.registerComponents(applicationContext, glide);
}
}
}
}
return glide;
}
...
}

我们来仔细看一下上面这段代码。首先这里使用了一个单例模式来获取 Glide 对象的实例,可以看到,这是一个非常典型的双重锁模式。然后在第 12 行,调用 ManifestParser 的 parse() 方法去解析 AndroidManifest.xml 文件中的配置,实际上就是将 AndroidManifest 中所有值为 GlideModule 的 meta-data 配置读取出来,并将相应的自定义模块实例化。由于你可以自定义任意多个模块,因此这里我们将会得到一个 GlideModule 的 List 集合。

接下来在第 13 行创建了一个 GlideBuilder 对象,并通过一个循环调用了每一个 GlideModule 的 applyOptions() 方法,同时也把 GlideBuilder 对象作为参数传入到这个方法中。而 applyOptions() 方法就是我们可以加入自己的逻辑的地方了,虽然目前为止我们还没有编写任何逻辑。

再往下的一步就非常关键了,这里调用了 GlideBuilder 的 createGlide() 方法,并返回了一个 Glide 对象。也就是说,Glide 对象的实例就是在这里创建的了,那么我们跟到这个方法当中瞧一瞧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class GlideBuilder {
private final Context context;
private Engine engine;
private BitmapPool bitmapPool;
private MemoryCache memoryCache;
private ExecutorService sourceService;
private ExecutorService diskCacheService;
private DecodeFormat decodeFormat;
private DiskCache.Factory diskCacheFactory;
...
Glide createGlide() {
if (sourceService == null) {
final int cores = Math.max(1, Runtime.getRuntime().availableProcessors());
sourceService = new FifoPriorityThreadPoolExecutor(cores);
}
if (diskCacheService == null) {
diskCacheService = new FifoPriorityThreadPoolExecutor(1);
}
MemorySizeCalculator calculator = new MemorySizeCalculator(context);
if (bitmapPool == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
int size = calculator.getBitmapPoolSize();
bitmapPool = new LruBitmapPool(size);
} else {
bitmapPool = new BitmapPoolAdapter();
}
}
if (memoryCache == null) {
memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
}
if (diskCacheFactory == null) {
diskCacheFactory = new InternalCacheDiskCacheFactory(context);
}
if (engine == null) {
engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
}
if (decodeFormat == null) {
decodeFormat = DecodeFormat.DEFAULT;
}
return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
}
}

这个方法中会创建 BitmapPool、MemoryCache、DiskCache、DecodeFormat 等对象的实例,并在最后一行创建一个 Glide 对象的实例,然后将前面创建的这些实例传入到 Glide 对象当中,以供后续的图片加载操作使用。

但是大家有没有注意到一个细节,createGlide() 方法中创建任何对象的时候都做了一个空检查,只有在对象为空的时候才会去创建它的实例。也就是说,如果我们可以在 applyOptions() 方法中提前就给这些对象初始化并赋值,那么在 createGlide() 方法中就不会再去重新创建它们的实例了,从而也就实现了更改 Glide 配置的功能。关于这个功能我们待会儿会进行具体的演示。

现在继续回到 Glide 的 get() 方法中,得到了 Glide 对象的实例之后,接下来又通过一个循环调用了每一个 GlideModule 的 registerComponents() 方法,在这里我们可以加入替换 Glide 的组件的逻辑。

好了,这就是 Glide 自定义模块的全部工作原理。了解了它的工作原理之后,接下来所有的问题就集中在我们到底如何在 applyOptions() 和 registerComponents() 这两个方法中加入具体的逻辑了,下面我们马上就来学习一下。

更改 Glide 配置

刚才在分析自定义模式工作原理的时候其实就已经提到了,如果想要更改 Glide 的默认配置,其实只需要在 applyOptions() 方法中提前将 Glide 的配置项进行初始化就可以了。那么 Glide 一共有哪些配置项呢?这里我给大家做了一个列举:

  • setMemoryCache()
    用于配置 Glide 的内存缓存策略,默认配置是 LruResourceCache。
  • setBitmapPool()
    用于配置 Glide 的 Bitmap 缓存池,默认配置是 LruBitmapPool。
  • setDiskCache()
    用于配置 Glide 的硬盘缓存策略,默认配置是 InternalCacheDiskCacheFactory。
  • setDiskCacheService()
    用于配置 Glide 读取缓存中图片的异步执行器,默认配置是 FifoPriorityThreadPoolExecutor,也就是先入先出原则。
  • setResizeService()
    用于配置 Glide 读取非缓存中图片的异步执行器,默认配置也是 FifoPriorityThreadPoolExecutor。
  • setDecodeFormat()
    用于配置 Glide 加载图片的解码模式,默认配置是 RGB_565。

其实 Glide 的这些默认配置都非常科学且合理,使用的缓存算法也都是效率极高的,因此在绝大多数情况下我们并不需要去修改这些默认配置,这也是 Glide 用法能如此简洁的一个原因。

但是 Glide 科学的默认配置并不影响我们去学习自定义 Glide 模块的功能,因此总有某些情况下,默认的配置可能将无法满足你,这个时候就需要我们自己动手来修改默认配置了。

下面就通过具体的实例来看一下吧。刚才说到,Glide 默认的硬盘缓存策略使用的是 InternalCacheDiskCacheFactory,这种缓存会将所有 Glide 加载的图片都存储到当前应用的私有目录下。这是一种非常安全的做法,但同时这种做法也造成了一些不便,因为私有目录下即使是开发者自己也是无法查看的,如果我想要去验证一下图片到底有没有成功缓存下来,这就有点不太好办了。

这种情况下,就非常适合使用自定义模块来更改 Glide 的默认配置。我们完全可以自己去实现 DiskCache.Factory 接口来自定义一个硬盘缓存策略,不过却大大没有必要这么做,因为 Glide 本身就内置了一个 ExternalCacheDiskCacheFactory,可以允许将加载的图片都缓存到 SD 卡。

那么接下来,我们就尝试使用这个 ExternalCacheDiskCacheFactory 来替换默认的 InternalCacheDiskCacheFactory,从而将所有 Glide 加载的图片都缓存到 SD 卡上。

由于在前面我们已经创建好了一个自定义模块 MyGlideModule,那么现在就可以直接在这里编写逻辑了,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyGlideModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setDiskCache(new ExternalCacheDiskCacheFactory(context));
}
@Override
public void registerComponents(Context context, Glide glide) {
}
}

没错,就是这么简单,现在所有Glide加载的图片都会缓存到SD卡上了。

另外,InternalCacheDiskCacheFactory 和 ExternalCacheDiskCacheFactory 的默认硬盘缓存大小都是 250M。也就是说,如果你的应用缓存的图片总大小超出了 250M,那么 Glide 就会按照 DiskLruCache 算法的原则来清理缓存的图片。

当然,我们是可以对这个默认的缓存大小进行修改的,而且修改方式非常简单,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyGlideModule implements GlideModule {
public static final int DISK_CACHE_SIZE = 500 * 1024 * 1024;
@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, DISK_CACHE_SIZE));
}
@Override
public void registerComponents(Context context, Glide glide) {
}
}

只需要向 ExternalCacheDiskCacheFactory 或者 InternalCacheDiskCacheFactory 再传入一个参数就可以了,现在我们就将 Glide 硬盘缓存的大小调整成了 500M。

好了,更改 Glide 配置的功能就是这么简单,那么接下来我们就来验证一下更改的配置到底有没有生效吧。

这里还是使用最基本的 Glide 加载语句来去加载一张网络图片:

1
2
3
4
String url = "http://guolin.tech/book.png";
Glide.with(this)
.load(url)
.into(imageView);

运行一下程序,效果如下图所示:

OK,现在图片已经加载出现了,那么我们去找一找它的缓存吧。

ExternalCacheDiskCacheFactory 的默认缓存路径是在 sdcard/Android/包名/cache/image_manager_disk_cache 目录当中,我们使用文件浏览器进入到这个目录,结果如下图所示。

可以看到,这里有两个文件,其中 journal 文件是 DiskLruCache 算法的日志文件,这个文件必不可少,且只会有一个。

而另外一个文件就是那张缓存的图片了,它的文件名虽然看上去很奇怪,但是我们只需要把这个文件的后缀改成 .png,然后用图片浏览器打开,结果就一目了然了,如下图所示。

由此证明,我们已经成功将 Glide 的硬盘缓存路径修改到 SD 卡上了。

另外这里再提一点,我们都知道 Glide 和 Picasso 的用法是非常相似的,但是有一点差别却很大。Glide 加载图片的默认格式是 RGB_565,而 Picasso 加载图片的默认格式是 ARGB_8888。ARGB_8888 格式的图片效果会更加细腻,但是内存开销会比较大。而 RGB_565 格式的图片则更加节省内存,但是图片效果上会差一些。

Glide 和 Picasso 各自采取的默认图片格式谈不上熟优熟劣,只能说各自的取舍不一样。但是如果你希望 Glide 也能使用 ARGB_8888 的图片格式,这当然也是可以的。我们只需要在 MyGlideModule 中更改一下默认配置即可,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyGlideModule implements GlideModule {
public static final int DISK_CACHE_SIZE = 500 * 1024 * 1024;
@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, DISK_CACHE_SIZE));
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
}
@Override
public void registerComponents(Context context, Glide glide) {
}
}

通过这样配置之后,使用 Glide 加载的所有图片都将会使用 ARGB_8888 的格式,虽然图片质量变好了,但同时内存开销也会明显增大,所以你要做好心理准备哦。

好了,关于更改 Glide 配置的内容就介绍这么多,接下来就让我们进入到下一个非常重要的主题,替换 Glide 组件。

替换 Glide 组件

替换 Glide 组件功能需要在自定义模块的 registerComponents() 方法中加入具体的替换逻辑。相比于更改 Glide 配置,替换 Glide 组件这个功能的难度就明显大了不少。Glide 中的组件非常繁多,也非常复杂,但其实大多数情况下并不需要我们去做什么替换。不过,有一个组件却有着比较大的替换需求,那就是 Glide 的 HTTP 通讯组件。

默认情况下,Glide 使用的是基于原生 HttpURLConnection 进行订制的 HTTP 通讯组件,但是现在大多数的 Android 开发者都更喜欢使用 OkHttp,因此将 Glide 中的 HTTP 通讯组件修改成 OkHttp 的这个需求比较常见,那么今天我们也会以这个功能来作为例子进行讲解。

首先来看一下 Glide 中目前有哪些组件吧,在 Glide 类的构造方法当中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Glide {
Glide(Engine engine, MemoryCache memoryCache, BitmapPool bitmapPool, Context context, DecodeFormat decodeFormat) {
...
register(File.class, ParcelFileDescriptor.class, new FileDescriptorFileLoader.Factory());
register(File.class, InputStream.class, new StreamFileLoader.Factory());
register(int.class, ParcelFileDescriptor.class, new FileDescriptorResourceLoader.Factory());
register(int.class, InputStream.class, new StreamResourceLoader.Factory());
register(Integer.class, ParcelFileDescriptor.class, new FileDescriptorResourceLoader.Factory());
register(Integer.class, InputStream.class, new StreamResourceLoader.Factory());
register(String.class, ParcelFileDescriptor.class, new FileDescriptorStringLoader.Factory());
register(String.class, InputStream.class, new StreamStringLoader.Factory());
register(Uri.class, ParcelFileDescriptor.class, new FileDescriptorUriLoader.Factory());
register(Uri.class, InputStream.class, new StreamUriLoader.Factory());
register(URL.class, InputStream.class, new StreamUrlLoader.Factory());
register(GlideUrl.class, InputStream.class, new HttpUrlGlideUrlLoader.Factory());
register(byte[].class, InputStream.class, new StreamByteArrayLoader.Factory());
...
}
}

可以看到,这里都是以调用 register() 方法的方式来注册一个组件, register() 方法中传入的参数表示 Glide 支持使用哪种参数类型来加载图片,以及如何去处理这种类型的图片加载。举个例子:

1
register(GlideUrl.class, InputStream.class, new HttpUrlGlideUrlLoader.Factory());

这句代码就表示,我们可以使用 Glide.with(context).load(new GlideUrl(“url…”)).into(imageView) 的方式来加载图片,而 HttpUrlGlideUrlLoader.Factory 则是要负责处理具体的网络通讯逻辑。如果我们想要将 Glide 的 HTTP 通讯组件替换成 OkHttp 的话,那么只需要在自定义模块当中重新注册一个 GlideUrl 类型的组件就行了。

说到这里有的朋友可能会疑问了,我们平时使用 Glide 加载图片时,大多数情况下都是直接将图片的 URL 字符串传入到 load() 方法当中的,很少会将它封装成 GlideUrl 对象之后再传入到 load() 方法当中,那为什么只需要重新注册一个 GlideUrl 类型的组件,而不需要去重新注册一个 String 类型的组件呢?其实道理很简单,因为 load(String) 方法只是 Glide 给我们提供一种简易的 API 封装而已,它的底层仍然还是调用的 GlideUrl 组件,因此我们在替换组件的时候只需要直接替换最底层的,这样就一步到位了。

那么接下来我们就开始学习到底如何将 Glide 的 HTTP 通讯组件替换成 OkHttp。

首先第一步,不用多说,肯定是要先将 OkHttp 的库引入到当前项目中,如下所示:

1
2
3
dependencies {
compile 'com.squareup.okhttp3:okhttp:3.9.0'
}

然后接下来该怎么做呢?我们只要依葫芦画瓢就可以了。刚才不是说 Glide 的网络通讯逻辑是由 HttpUrlGlideUrlLoader.Factory 来负责的吗,那么我们就来看一下它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class HttpUrlGlideUrlLoader implements ModelLoader<GlideUrl, InputStream> {
private final ModelCache<GlideUrl, GlideUrl> modelCache;
public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
private final ModelCache<GlideUrl, GlideUrl> modelCache = new ModelCache<GlideUrl, GlideUrl>(500);
@Override
public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
return new HttpUrlGlideUrlLoader(modelCache);
}
@Override
public void teardown() {
}
}
public HttpUrlGlideUrlLoader() {
this(null);
}
public HttpUrlGlideUrlLoader(ModelCache<GlideUrl, GlideUrl> modelCache) {
this.modelCache = modelCache;
}
@Override
public DataFetcher<InputStream> getResourceFetcher(GlideUrl model, int width, int height) {
GlideUrl url = model;
if (modelCache != null) {
url = modelCache.get(model, 0, 0);
if (url == null) {
modelCache.put(model, 0, 0, model);
url = model;
}
}
return new HttpUrlFetcher(url);
}
}

可以看到,HttpUrlGlideUrlLoader.Factory 是一个内部类,外层的 HttpUrlGlideUrlLoader 类实现了 ModelLoader 这个接口,并重写了 getResourceFetcher() 方法。而在 getResourceFetcher() 方法中,又创建了一个 HttpUrlFetcher 的实例,在这里才是真正处理具体网络通讯逻辑的地方,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
public class HttpUrlFetcher implements DataFetcher<InputStream> {
private static final String TAG = "HttpUrlFetcher";
private static final int MAXIMUM_REDIRECTS = 5;
private static final HttpUrlConnectionFactory DEFAULT_CONNECTION_FACTORY = new DefaultHttpUrlConnectionFactory();
private final GlideUrl glideUrl;
private final HttpUrlConnectionFactory connectionFactory;
private HttpURLConnection urlConnection;
private InputStream stream;
private volatile boolean isCancelled;
public HttpUrlFetcher(GlideUrl glideUrl) {
this(glideUrl, DEFAULT_CONNECTION_FACTORY);
}
HttpUrlFetcher(GlideUrl glideUrl, HttpUrlConnectionFactory connectionFactory) {
this.glideUrl = glideUrl;
this.connectionFactory = connectionFactory;
}
@Override
public InputStream loadData(Priority priority) throws Exception {
return loadDataWithRedirects(glideUrl.toURL(), 0 , null , glideUrl.getHeaders());
}
private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map<String, String> headers)
throws IOException {
if (redirects >= MAXIMUM_REDIRECTS) {
throw new IOException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!");
} else {
try {
if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) {
throw new IOException("In re-direct loop");
}
} catch (URISyntaxException e) {
}
}
urlConnection = connectionFactory.build(url);
for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
}
urlConnection.setConnectTimeout(2500);
urlConnection.setReadTimeout(2500);
urlConnection.setUseCaches(false);
urlConnection.connect();
if (isCancelled) {
return null;
}
final int statusCode = urlConnection.getResponseCode();
if (statusCode / 100 == 2) {
return getStreamForSuccessfulRequest(urlConnection);
} else if (statusCode / 100 == 3) {
String redirectUrlString = urlConnection.getHeaderField("Location");
if (TextUtils.isEmpty(redirectUrlString)) {
throw new IOException("Received empty or null redirect url");
}
URL redirectUrl = new URL(url, redirectUrlString);
return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
} else {
if (statusCode == -1) {
throw new IOException("Unable to retrieve response code from HttpUrlConnection.");
}
throw new IOException("Request failed " + statusCode + ": " + urlConnection.getResponseMessage());
}
}
private InputStream getStreamForSuccessfulRequest(HttpURLConnection urlConnection)
throws IOException {
if (TextUtils.isEmpty(urlConnection.getContentEncoding())) {
int contentLength = urlConnection.getContentLength();
stream = ContentLengthInputStream.obtain(urlConnection.getInputStream(), contentLength);
} else {
stream = urlConnection.getInputStream();
}
return stream;
}
@Override
public void cleanup() {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
}
}
if (urlConnection != null) {
urlConnection.disconnect();
}
}
@Override
public String getId() {
return glideUrl.getCacheKey();
}
@Override
public void cancel() {
isCancelled = true;
}
interface HttpUrlConnectionFactory {
HttpURLConnection build(URL url) throws IOException;
}
private static class DefaultHttpUrlConnectionFactory implements HttpUrlConnectionFactory {
@Override
public HttpURLConnection build(URL url) throws IOException {
return (HttpURLConnection) url.openConnection();
}
}
}

上面这段代码看上去应该不费力吧?其实就是一些 HttpURLConnection 的用法而已。那么我们只需要仿照着 HttpUrlFetcher 的代码来写,并且把 HTTP 的通讯组件替换成 OkHttp 就可以了。

现在新建一个 OkHttpFetcher 类,并且同样实现 DataFetcher 接口,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class OkHttpFetcher implements DataFetcher<InputStream> {
private final OkHttpClient client;
private final GlideUrl url;
private InputStream stream;
private ResponseBody responseBody;
private volatile boolean isCancelled;
public OkHttpFetcher(OkHttpClient client, GlideUrl url) {
this.client = client;
this.url = url;
}
@Override
public InputStream loadData(Priority priority) throws Exception {
Request.Builder requestBuilder = new Request.Builder()
.url(url.toStringUrl());
for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
String key = headerEntry.getKey();
requestBuilder.addHeader(key, headerEntry.getValue());
}
requestBuilder.addHeader("httplib", "OkHttp");
Request request = requestBuilder.build();
if (isCancelled) {
return null;
}
Response response = client.newCall(request).execute();
responseBody = response.body();
if (!response.isSuccessful() || responseBody == null) {
throw new IOException("Request failed with code: " + response.code());
}
stream = ContentLengthInputStream.obtain(responseBody.byteStream(),
responseBody.contentLength());
return stream;
}
@Override
public void cleanup() {
try {
if (stream != null) {
stream.close();
}
if (responseBody != null) {
responseBody.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String getId() {
return url.getCacheKey();
}
@Override
public void cancel() {
isCancelled = true;
}
}

上面这段代码完全就是我照着 HttpUrlFetcher 依葫芦画瓢写出来的,用的也都是一些 OkHttp 的基本用法,相信不需要再做什么解释了吧。可以看到,使用 OkHttp 来编写网络通讯的代码要比使用 HttpURLConnection 简单很多,代码行数也少了很多。注意在第 22 行,我添加了一个 httplib: OkHttp 的请求头,这个是待会儿我们用来进行测试验证的,大家实际项目中的代码无须添加这个请求头。

那么我们就继续发挥依葫芦画瓢的精神,仿照着 HttpUrlGlideUrlLoader 再写一个 OkHttpGlideUrlLoader 吧。新建一个 OkHttpGlideUrlLoader 类,并且实现 ModelLoader 接口,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class OkHttpGlideUrlLoader implements ModelLoader<GlideUrl, InputStream> {
private OkHttpClient okHttpClient;
public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
private OkHttpClient client;
public Factory() {
}
public Factory(OkHttpClient client) {
this.client = client;
}
private synchronized OkHttpClient getOkHttpClient() {
if (client == null) {
client = new OkHttpClient();
}
return client;
}
@Override
public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
return new OkHttpGlideUrlLoader(getOkHttpClient());
}
@Override
public void teardown() {
}
}
public OkHttpGlideUrlLoader(OkHttpClient client) {
this.okHttpClient = client;
}
@Override
public DataFetcher<InputStream> getResourceFetcher(GlideUrl model, int width, int height) {
return new OkHttpFetcher(okHttpClient, model);
}
}

注意这里的 Factory 我提供了两个构造方法,一个是不带任何参数的,一个是带 OkHttpClient 参数的。如果对 OkHttp 不需要进行任何自定义的配置,那么就调用无参的 Factory 构造函数即可,这样会在内部自动创建一个 OkHttpClient 实例。但如果你需要想添加拦截器,或者修改 OkHttp 的默认超时等等配置,那么就自己创建一个 OkHttpClient 的实例,然后传入到 Factory 的构造方法当中就行了。

好了,现在就只差最后一步,将我们刚刚创建的 OkHttpGlideUrlLoader和OkHttpFetcher 注册到 Glide 当中,将原来的 HTTP 通讯组件给替换掉,如下所示:

1
2
3
4
5
6
7
8
9
10
public class MyGlideModule implements GlideModule {
...
@Override
public void registerComponents(Context context, Glide glide) {
glide.register(GlideUrl.class, InputStream.class, new OkHttpGlideUrlLoader.Factory());
}
}

可以看到,这里也是调用了 Glide 的 register() 方法来注册组件的。register() 方法中使用的 Map 类型来存储已注册的组件,因此我们这里重新注册了一遍 GlideUrl.class 类型的组件,就把原来的组件给替换掉了。

理论上来说,现在我们已经成功将 Glide 的 HTTP 通讯组件替换成 OkHttp了,现在唯一的问题就是我们该如何去验证一下到底有没有替换成功呢?

验证的方式我倒是想了很多种,比如添加 OkHttp 拦截器,或者自己架设一个测试用的服务器都是可以的。不过为了让大家最直接地看到验证结果,这里我准备使用 Fiddler 这个抓包工具来进行验证。这个工具的用法非常简单,但是限于篇幅我就不在本篇文章中介绍这个工具的用法了,还没用过这个工具的朋友们可以通过 这篇文章 了解一下。

在开始验证之前,我们还得要再修改一下 Glide 加载图片的代码才行,如下所示:

1
2
3
4
5
6
String url = "http://guolin.tech/book.png";
Glide.with(this)
.load(url)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(imageView);

这里我把 Glide 的内存缓存和硬盘缓存都禁用掉了,不然的话, Glide 可能会直接读取刚才缓存的图片,而不会再重新发起网终请求。

好的,现在我们重新使用 Glide 加载一下图片,然后观察 Fiddler 中的抓包情况,如下图所示。

可以看到,在 HTTP 请求头中确实有我们刚才自己添加的 httplib: OkHttp。也就说明,Glide 的 HTTP 通讯组件的确被替换成功了。

更简单的组件替换

上述方法是我们纯手工地将 Glide 的 HTTP 通讯组件进行了替换,如果你不想这么麻烦也是可以的,Glide 官方给我们提供了非常简便的 HTTP 组件替换方式。并且除了支持 OkHttp3 之外,还支持 OkHttp2 和 Volley。

我们只需要在 gradle 当中添加几行库的配置就行了。比如使用 OkHttp3 来作为 HTTP 通讯组件的配置如下:

1
2
3
4
dependencies {
compile 'com.squareup.okhttp3:okhttp:3.9.0'
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar'
}

使用 OkHttp2 来作为 HTTP 通讯组件的配置如下:

1
2
3
4
dependencies {
compile 'com.github.bumptech.glide:okhttp-integration:1.5.0@aar'
compile 'com.squareup.okhttp:okhttp:2.7.5'
}

使用 Volley 来作为 HTTP 通讯组件的配置如下:

1
2
3
4
dependencies {
compile 'com.github.bumptech.glide:volley-integration:1.5.0@aar'
compile 'com.mcxiaoke.volley:library:1.0.19'
}

当然了,这些库背后的工作原理和我们刚才自己手动实现替换 HTTP 组件的原理是一模一样的。而学会了手动替换组件的原理我们就能更加轻松地扩展更多丰富的功能,因此掌握这一技能还是非常重要的。